/**
 * github https://github.com/kartena/Proj4Leaflet
 */
import L from 'leaflet';
import proj4 from 'proj4';


L.Proj = {};

L.Proj._isProj4Obj = function(a) {
    return (typeof a.inverse !== 'undefined' &&
        typeof a.forward !== 'undefined');
};

L.Proj.Projection = L.Class.extend({
    initialize: function(code, def, bounds) {
        var isP4 = L.Proj._isProj4Obj(code);
        this._proj = isP4 ? code : this._projFromCodeDef(code, def);
        this.bounds = isP4 ? def : bounds;
    },

    project: function(latlng) {
        var point = this._proj.forward([latlng.lng, latlng.lat]);
        return new L.Point(point[0], point[1]);
    },

    unproject: function(point, unbounded) {
        var point2 = this._proj.inverse([point.x, point.y]);
        return new L.LatLng(point2[1], point2[0], unbounded);
    },

    _projFromCodeDef: function(code, def) {
        if (def) {
            proj4.defs(code, def);
        } else if (proj4.defs[code] === undefined) {
            var urn = code.split(':');
            if (urn.length > 3) {
                code = urn[urn.length - 3] + ':' + urn[urn.length - 1];
            }
            if (proj4.defs[code] === undefined) {
                throw 'No projection definition for code ' + code;
            }
        }

        return proj4(code);
    }
});

L.Proj.CRS = L.Class.extend({
    includes: L.CRS,

    options: {
        transformation: new L.Transformation(1, 0, -1, 0)
    },

    initialize: function(a, b, c) {
        var code,
            proj,
            def,
            options;

        if (L.Proj._isProj4Obj(a)) {
            proj = a;
            code = proj.srsCode;
            options = b || {};

            this.projection = new L.Proj.Projection(proj, options.bounds);
        } else {
            code = a;
            def = b;
            options = c || {};
            this.projection = new L.Proj.Projection(code, def, options.bounds);
        }

        L.Util.setOptions(this, options);
        this.code = code;
        this.transformation = this.options.transformation;

        if (this.options.origin) {
            this.transformation =
                new L.Transformation(1, -this.options.origin[0], -1, this.options.origin[1]);
        }

        if (this.options.scales) {
            this._scales = this.options.scales;
        } else if (this.options.resolutions) {
            this._scales = [];
            for (var i = this.options.resolutions.length - 1; i >= 0; i--) {
                if (this.options.resolutions[i]) {
                    this._scales[i] = 1 / this.options.resolutions[i];
                }
            }
        }

        this.infinite = !this.options.bounds;

    },

    scale: function(zoom) {
        var iZoom = Math.floor(zoom),
            baseScale,
            nextScale,
            scaleDiff,
            zDiff;
        if (zoom === iZoom) {
            return this._scales[zoom];
        } else {
            // Non-integer zoom, interpolate
            baseScale = this._scales[iZoom];
            nextScale = this._scales[iZoom + 1];
            scaleDiff = nextScale - baseScale;
            zDiff = (zoom - iZoom);
            return baseScale + scaleDiff * zDiff;
        }
    },

    zoom: function(scale) {
        // Find closest number in this._scales, down
        var downScale = this._closestElement(this._scales, scale),
            downZoom = this._scales.indexOf(downScale),
            nextScale,
            nextZoom,
            scaleDiff;
        // Check if scale is downScale => return array index
        if (scale === downScale) {
            return downZoom;
        }
        if (downScale === undefined) {
            return -Infinity;
        }
        // Interpolate
        nextZoom = downZoom + 1;
        nextScale = this._scales[nextZoom];
        if (nextScale === undefined) {
            return Infinity;
        }
        scaleDiff = nextScale - downScale;
        return (scale - downScale) / scaleDiff + downZoom;
    },

    distance: L.CRS.Earth.distance,

    R: L.CRS.Earth.R,

    /* Get the closest lowest element in an array */
    _closestElement: function(array, element) {
        var low;
        for (var i = array.length; i--;) {
            if (array[i] <= element && (low === undefined || low < array[i])) {
                low = array[i];
            }
        }
        return low;
    }
});

L.Proj.GeoJSON = L.GeoJSON.extend({
    initialize: function(geojson, options) {
        this._callLevel = 0;
        L.GeoJSON.prototype.initialize.call(this, geojson, options);
    },

    addData: function(geojson) {
        var crs;

        if (geojson) {
            if (geojson.crs && geojson.crs.type === 'name') {
                crs = new L.Proj.CRS(geojson.crs.properties.name);
            } else if (geojson.crs && geojson.crs.type) {
                crs = new L.Proj.CRS(geojson.crs.type + ':' + geojson.crs.properties.code);
            }

            if (crs !== undefined) {
                this.options.coordsToLatLng = function(coords) {
                    var point = L.point(coords[0], coords[1]);
                    return crs.projection.unproject(point);
                };
            }
        }

        // Base class' addData might call us recursively, but
        // CRS shouldn't be cleared in that case, since CRS applies
        // to the whole GeoJSON, inluding sub-features.
        this._callLevel++;
        try {
            L.GeoJSON.prototype.addData.call(this, geojson);
        } finally {
            this._callLevel--;
            if (this._callLevel === 0) {
                delete this.options.coordsToLatLng;
            }
        }
    }
});

L.Proj.geoJson = function(geojson, options) {
    return new L.Proj.GeoJSON(geojson, options);
};

L.Proj.ImageOverlay = L.ImageOverlay.extend({
    initialize: function(url, bounds, options) {
        L.ImageOverlay.prototype.initialize.call(this, url, null, options);
        this._projectedBounds = bounds;
    },

    // Danger ahead: Overriding internal methods in Leaflet.
    // Decided to do this rather than making a copy of L.ImageOverlay
    // and doing very tiny modifications to it.
    // Future will tell if this was wise or not.
    _animateZoom: function(event) {
        var scale = this._map.getZoomScale(event.zoom);
        var northWest = L.point(this._projectedBounds.min.x, this._projectedBounds.max.y);
        var offset = this._projectedToNewLayerPoint(northWest, event.zoom, event.center);

        L.DomUtil.setTransform(this._image, offset, scale);
    },

    _reset: function() {
        var zoom = this._map.getZoom();
        var pixelOrigin = this._map.getPixelOrigin();
        var bounds = L.bounds(
            this._transform(this._projectedBounds.min, zoom)._subtract(pixelOrigin),
            this._transform(this._projectedBounds.max, zoom)._subtract(pixelOrigin)
        );
        var size = bounds.getSize();

        L.DomUtil.setPosition(this._image, bounds.min);
        this._image.style.width = size.x + 'px';
        this._image.style.height = size.y + 'px';
    },

    _projectedToNewLayerPoint: function(point, zoom, center) {
        var viewHalf = this._map.getSize()._divideBy(2);
        var newTopLeft = this._map.project(center, zoom)._subtract(viewHalf)._round();
        var topLeft = newTopLeft.add(this._map._getMapPanePos());

        return this._transform(point, zoom)._subtract(topLeft);
    },

    _transform: function(point, zoom) {
        var crs = this._map.options.crs;
        var transformation = crs.transformation;
        var scale = crs.scale(zoom);

        return transformation.transform(point, scale);
    }
});

L.Proj.imageOverlay = function(url, bounds, options) {
    return new L.Proj.ImageOverlay(url, bounds, options);
};

export default L.Proj;